#region Copyright and Licensing Notice
// Copyright (C) 2011 Steven Bosnick
// 
// This program is open source software; you can redistribute it 
// and/or modifiy it under the terms of the Eclipse Public License,
// version 1.0.
// 
// This program is distributed in the hope that it will be useful, 
// but WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A 
// PARTICULAR PURPOSE. See the Eclipse Public License for more
// details.
// 
// You should have received a copy of the Eclipse Public License 
// along with this program; if not, you can find a copy at
// "http://www.opensource.org/licenses/eclipse-1.0.php".
#endregion
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BitwiseOre.SharedMemory.Core;
using BitwiseOre.SharedMemory.Core.Common;
using BitwiseOre.SharedMemory.Core.IPCQueues;
using NUnit.Framework;
using Moq;

namespace BitwiseOre.Tests.SharedMemory.Connection
{
    [TestFixture]
    public sealed class WriterFixture : AssertionHelper
    {
        [Test(Description = "Writing an array segment uses an unstarted task.")]
        public void Write_UsesUnstartedTask()
        {
            var writer = new Writer(CreateQueues(), _ => null);
            var task = writer.Write(new ArraySegment<byte>(new byte[] {1, 2, 3, 4}), TimeSpan.FromMinutes(1));

            Expect(task.Status, EqualTo(TaskStatus.Created), "Write task started upon retrun from write.");
        }

        [Test(Description = "Writting an array segment creates a new timeout helper.")]
        public void Write_CreatesTimeoutHelper()
        {
            var expectedtimeout = TimeSpan.FromMinutes(1);
            TimeSpan? timeout = null;
            Func<TimeSpan, ITimeoutHelper> creator =
                ts =>
                    {
                        timeout = ts;
                        return new Mock<ITimeoutHelper>().Object;
                    };

            var writer = new Writer(CreateQueues(), creator);
            writer.Write(new ArraySegment<byte>(new byte[] {1, 2, 3, 4}), expectedtimeout);

            Expect(timeout.HasValue, "No timeout helper created.");
            Expect(timeout.Value == expectedtimeout, "Unexpected timeout used to create timeout helper.");
        }

        [Test(Description = "Writing an array segment delegates to the current write buffer if it is not null.")]
        public void Write_DelegatesToNonNullWriteBuffer()
        {
            var array = new ArraySegment<byte>(new byte[] {1, 2, 3, 4});
            var writebuffers = new Mock<IGuardedEnumerator<IWriteBuffer>>();
            writebuffers.Setup(w => w.Current.WriteArraySegment(It.IsAny<ArraySegment<byte>>()))
                .Returns((ArraySegment<byte> a) => a.Count);

            var writer = new Writer(CreateQueues(writebuffers.Object), ts => new Mock<ITimeoutHelper>().Object);
            writer.Write(array, TimeSpan.FromMinutes(1)).RunSynchronously();

            writebuffers.Verify(w => w.Current.WriteArraySegment(It.Is<ArraySegment<byte>>(a => a == array)),
                          "No delegated call to write the array semgment to the current write buffer.");
        }

        [Test(Description = "Writing an array segment moves to the next write buffer if the current one is null.")]
        public void Write_MovesToNextWriteBufferIfCurrentNull()
        {
            var timeout = TimeSpan.FromMinutes(1);
            var helper = new Mock<ITimeoutHelper>();
            helper.SetupGet(h => h.RemainingTime).Returns(timeout);
            var writebuffers = new Mock<IGuardedEnumerator<IWriteBuffer>>();
            writebuffers.SetupGet(w => w.Current).Returns((IWriteBuffer) null);

            var writer = new Writer(CreateQueues(writebuffers.Object), ts => helper.Object);
            writer.Write(new ArraySegment<byte>(new byte[] {1, 2, 3, 4}), TimeSpan.FromMinutes(2))
                .RunSynchronously();
            
            writebuffers.Verify(w => w.MoveNext(It.Is<TimeSpan>(t => t == timeout)),
                          "Writing when the current write buffer is null did not move to the next write buffer.");
        }

        [Test(Description = "Writing an array segement writes to the next write buffer if the current one is null.")]
        public void Write_DelegatesToNextWriteBufferIfCurrentNull()
        {
            var helper = new Mock<ITimeoutHelper>();
            helper.SetupGet(h => h.RemainingTime).Returns(TimeSpan.FromMinutes(1));
            var writebuffer = new Mock<IWriteBuffer>();
            var currentvalues = new Queue<IWriteBuffer>(new[] {null, writebuffer.Object, writebuffer.Object});
            var writebuffers = new Mock<IGuardedEnumerator<IWriteBuffer>>();
            writebuffers.SetupGet(w => w.Current).Returns(currentvalues.Dequeue);
            writebuffers.Setup(w => w.MoveNext(It.IsAny<TimeSpan>()))
                .Returns(Task.Factory.StartNew(() => true));
            var array = new ArraySegment<byte>(new byte[] {1, 2, 3, 4});

            var writer = new Writer(CreateQueues(writebuffers.Object), ts => helper.Object);
            writer.Write(array, TimeSpan.FromMinutes(2)).RunSynchronously();

            writebuffer.Verify(wb => wb.WriteArraySegment(It.Is<ArraySegment<byte>>(a => a == array)),
                               "There was no attempt to write to the next write buffer with the " + 
                               "expected parameters.");
        }

        [Test(Description = "Writting an array segment moves to the next write buffer if the write to " +
                            "the current one does not write the entire segment.")]
        public void Write_MovesToNextWriteBufferIfInitialWriteIncomplete()
        {
            var array = new ArraySegment<byte>(new byte[] {1, 2, 3, 4, 5, 6, 7, 8});
            var timeout = TimeSpan.FromMinutes(1);
            var helper = new Mock<ITimeoutHelper>();
            helper.SetupGet(h => h.RemainingTime).Returns(timeout);
            var writebuffers = new Mock<IGuardedEnumerator<IWriteBuffer>>();
            var results = new Queue<int>(new[] { array.Count - 2, 2 });
            writebuffers.Setup(w => w.Current.WriteArraySegment(It.IsAny<ArraySegment<byte>>()))
                .Returns(results.Dequeue);

            var writer = new Writer(CreateQueues(writebuffers.Object), ts => helper.Object);
            writer.Write(array, TimeSpan.FromMinutes(2))
                .RunSynchronously();
            
            writebuffers.Verify(w => w.MoveNext(It.Is<TimeSpan>(t => t == timeout)),
                          "Writing when the current write buffer fills up did not move to the next write buffer.");
            
        }

        [Test(Description = "Writing an array segement writes the remainder of the segment to the next " +
                            "write buffer if the current one fills up.")]
        public void Write_DelegatesToNextWriteBufferIfInitialWriteIncomplete()
        {
            var array = new ArraySegment<byte>(new byte[] {1, 2, 3, 4, 5, 6, 7, 8});
            var helper = new Mock<ITimeoutHelper>();
            helper.SetupGet(h => h.RemainingTime).Returns(TimeSpan.FromMinutes(1));
            var writebuffers = new Mock<IGuardedEnumerator<IWriteBuffer>>();
            const int remaining = 2;
            var results = new Queue<int>(new[] {array.Count - remaining, remaining});
            writebuffers.Setup(w => w.Current.WriteArraySegment(It.IsAny<ArraySegment<byte>>()))
                .Returns(results.Dequeue);
            writebuffers.Setup(w => w.MoveNext(It.IsAny<TimeSpan>()))
                .Returns(Task.Factory.StartNew(() => true));

            var writer = new Writer(CreateQueues(writebuffers.Object), ts => helper.Object);
            writer.Write(array, TimeSpan.FromMinutes(2))
                .RunSynchronously();

            writebuffers.Verify(
                w =>
                w.Current.WriteArraySegment(
                    It.Is<ArraySegment<byte>>(
                        a =>
                        a.Array == array.Array && a.Count == remaining &&
                        a.Offset == array.Offset + array.Count - remaining)),
                "No attempt was made to write to the write buffer with the remainder of the array segment.");
        }

        [Test(Description = "Writing a byte delegates to the current write buffer if it is not null.")]
        public void WriteByte_DelegatesToCurrentWriteBuffer()
        {
            var writebuffers = new Mock<IGuardedEnumerator<IWriteBuffer>>();
            writebuffers.Setup(w => w.Current.WriteByte(It.IsAny<byte>())).Returns(true);
            const byte value = 5;

            var writer = new Writer(CreateQueues(writebuffers.Object), ts => new Mock<ITimeoutHelper>().Object);
            writer.WriteByte(value, TimeSpan.FromMinutes(1));

            writebuffers.Verify(w => w.Current.WriteByte(It.Is<byte>(b => b == value)),
                          "Expected byte was not written to the current buffer.");
        }

        [Test(Description = "Writing a byte moves to the next write buffer is the current one it null.")]
        public void WriteByte_MovesToNextBufferIfNoCurrentBuffer()
        {
            var writebuffers = new Mock<IGuardedEnumerator<IWriteBuffer>>();
            var buffer = new Mock<IWriteBuffer>();
            var buffers = new Queue<IWriteBuffer>(new[] {null, null, buffer.Object, buffer.Object});
            writebuffers.SetupGet(w => w.Current).Returns(buffers.Dequeue);
            var results = new Queue<int>(new[] {0, 1});
            writebuffers.Setup(w => w.Current.WriteArraySegment(It.IsAny<ArraySegment<byte>>()))
                .Returns(results.Dequeue);
            writebuffers.Setup(w => w.MoveNext(It.IsAny<TimeSpan>()))
                .Returns(Task.Factory.StartNew(() => true));

            var writer = new Writer(CreateQueues(writebuffers.Object), ts => new Mock<ITimeoutHelper>().Object);
            writer.WriteByte(5, TimeSpan.FromMinutes(1));
            
            writebuffers.Verify(w => w.MoveNext(It.IsAny<TimeSpan>()),
                          "Writing a byte did not move to the next write buffer.");
        }

        [Test(Description = "Writing a byte moves to the next write buffer if the write to the current " +
                            "write buffer fails.")]
        public void WriteByte_MovesToNextBufferIfWriteToCurrentBufferFails()
        {
            var writebuffers = new Mock<IGuardedEnumerator<IWriteBuffer>>();
            writebuffers.Setup(w => w.Current.WriteByte(It.IsAny<byte>())).Returns(false);
            var results = new Queue<int>(new[] {0, 1});
            writebuffers.Setup(w => w.Current.WriteArraySegment(It.IsAny<ArraySegment<byte>>()))
                .Returns(results.Dequeue);
            writebuffers.Setup(w => w.MoveNext(It.IsAny<TimeSpan>()))
                .Returns(Task.Factory.StartNew(() => true));

            var writer = new Writer(CreateQueues(writebuffers.Object), ts => new Mock<ITimeoutHelper>().Object);
            writer.WriteByte(5, TimeSpan.FromMinutes(1));
            
            writebuffers.Verify(w => w.MoveNext(It.IsAny<TimeSpan>()),
                          "Writing a byte did not move to the next write buffer.");
        }

        [Test(Description = "Writing a byte propegates exceptions caused by moving to the next buffer timing out.")]
        public void WriteByte_PropegatesExceptionsIfMovingToNextBufferTimesOut()
        {
            var writebuffers = new Mock<IGuardedEnumerator<IWriteBuffer>>();
            writebuffers.Setup(w => w.Current.WriteByte(It.IsAny<byte>())).Returns(false);
            var results = new Queue<int>(new[] {0, 1});
            writebuffers.Setup(w => w.Current.WriteArraySegment(It.IsAny<ArraySegment<byte>>()))
                .Returns(results.Dequeue);
            writebuffers.Setup(w => w.MoveNext(It.IsAny<TimeSpan>()))
                .Returns(Task<bool>.Factory.StartNew(() =>{ throw new TimeoutException(); }));

            var writer = new Writer(CreateQueues(writebuffers.Object), ts => new Mock<ITimeoutHelper>().Object);
            
            Expect(() => writer.WriteByte(5, TimeSpan.FromMinutes(1)), Throws.InstanceOf<AggregateException>());
        }


        private static IConnectionQueues CreateQueues(IGuardedEnumerator<IWriteBuffer> writeBuffers = null)
        {
            writeBuffers = writeBuffers ??
                           new Mock<IGuardedEnumerator<IWriteBuffer>> {DefaultValue = DefaultValue.Mock}.Object;
            return new ConnectionQueues(writeBuffers, null);
        }
    }
}